Authentication
W3DS uses cryptographic signature-based authentication. Users authenticate with platforms by signing a session ID with their private key, which platforms verify using public keys stored in eVaults.
Overview
Unlike traditional password-based authentication, W3DS uses cryptographic signatures for authentication. This provides:
- No passwords: Users never share secrets with platforms
- Cryptographic proof: Platforms can cryptographically verify user identity
- Key-based: Uses ECDSA P-256 keys managed by the eID wallet
- Decentralized: Public keys stored in user's eVault, verified via Registry
Authentication Flow
The authentication process follows these steps:
Protocol Steps
Step 1: Platform Requests Session
When a user wants to log in, the platform must:
-
Generate a unique session identifier: Use a cryptographically secure random session ID (128-bit). This session ID will be signed by the user, so it must be unique and unpredictable.
-
Construct the redirect URL: Build the full URL where the eID wallet will POST the signed session data. This is typically
{platformBaseUrl}/api/auth/loginor similar. The wallet will make an HTTP POST request to this URL with the signed authentication data. -
Build the w3ds://auth URI: Create a URI with the following format:
w3ds://auth?redirect={redirectUrl}&session={sessionId}&platform={platformName}redirect: URL-encoded redirect endpoint where the eID wallet will POST the signed session (this is the callback URL)session: The generated session IDplatform: Platform identifier (for display purposes)
-
Return JSON response: Send a JSON object with the
urifield containing the w3ds://auth URI.
HTTP Endpoint: GET /api/auth/offer
Request: No body required, standard HTTP GET
Response Format:
{
"uri": "w3ds://auth?redirect=https://blabsy.example.com/api/auth&session=550e8400-e29b-41d4-a716-446655440000&platform=blabsy"
}
Implementation Requirements:
- Generate a cryptographically secure random session ID (128-bit)
- URL-encode the redirect parameter
- Return JSON with Content-Type: application/json
- Store the session ID temporarily (in memory, cache, or database) to validate it later
Reference Implementation (TypeScript):
getOffer = async (req: Request, res: Response) => {
const url = new URL(
"/api/auth",
process.env.PUBLIC_BLABSY_BASE_URL
).toString();
const session = uuidv4();
const offer = `w3ds://auth?redirect=${url}&session=${session}&platform=blabsy`;
res.json({ uri: offer });
};
Step 2: User Signs Session ID
The user's eID wallet signs the session ID using their private key. The signing process:
-
Parse the w3ds://auth URI: Extract the
sessionparameter from the query string. The session ID must be signed exactly as received. -
Hash the session ID: Convert the session ID string to bytes using UTF-8 encoding, then compute SHA-256 hash. This produces a 32-byte hash digest.
-
Sign the hash: Use ECDSA P-256 (secp256r1 curve) to sign the hash with the user's private key:
- Algorithm: ECDSA
- Curve: P-256 (secp256r1, NIST P-256)
- Hash: SHA-256
- Result: A signature consisting of two 32-byte integers (r and s), concatenated to form a 64-byte raw signature
-
Encode the signature:
- Software keys: Encode the 64-byte signature using base64 encoding
- Hardware keys: Encode using multibase base58btc (starts with 'z' prefix)
Cryptographic Details:
- Curve: secp256r1 (NIST P-256)
- Hash Algorithm: SHA-256
- Signature Format: Raw 64-byte (r || s), where r and s are each 32 bytes
- Encoding: Base64 (software) or Multibase base58btc (hardware)
Library Requirements (for wallet implementation):
- ECDSA signing library (crypto libraries in most languages support this)
- SHA-256 hashing
- Base64 or base58btc encoding
- Key management for storing/accessing private keys securely
For detailed signature format information, see the Signature Formats documentation.
Step 3: eID Wallet POSTs Signed Session
After the user signs the session ID, the eID wallet makes an HTTP POST request to the redirect URL specified in the w3ds://auth URI. The platform receives this POST request with the signed session data:
Endpoint: The redirect URL from the w3ds://auth URI (e.g., POST /api/auth/login)
What the eID Wallet POSTs:
Request Headers:
Content-Type: application/json
Request Body (JSON):
{
"w3id": "@user-a.w3id",
"session": "550e8400-e29b-41d4-a716-446655440000",
"signature": "xK3vJZQ2F3k5L8mN9pQrS7tUvW1xY3zA5bC7dE9fG1hIjKlMnOpQrStUvWxYz==",
"appVersion": "0.4.0"
}
Field Descriptions:
w3id: The user's W3ID (eName) identifier, always starts with '@'session: The session ID that was generated in Step 1signature: The base64 or multibase-encoded signature of the session IDappVersion: Optional version string for compatibility checking
Note on
appVersion: This field is temporary and will be sunset after the rollout is completed. It was added because some users were on outdated eID wallet versions that handled signing differently. Once all users have updated to compatible versions, this field will be removed from the protocol.
Platform Processing Steps:
-
Validate Input: Ensure all required fields (w3id, session, signature) are present and non-empty. Return 400 Bad Request if validation fails.
-
Validate Session: Check that the session ID matches one that was recently generated (within the last 5 minutes). Reject duplicate or expired sessions. This prevents replay attacks.
-
Verify Signature: Call the signature verification function (see Signing documentation) with:
- The user's eName
- The signature string
- The session ID as the payload
- The Registry base URL
-
Handle Verification Result:
- If verification succeeds: Create a session token (JWT, session cookie, or platform-specific token) and return it
- If verification fails: Return 401 Unauthorized with an error message
-
Optional App Version Check: Validate that appVersion meets minimum requirements. Return 400 if version is too old. Note: This check is temporary and will be removed after the rollout is completed (see note on
appVersionfield above).
Response on Success (200 OK):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Response on Failure (401 Unauthorized):
{
"error": "Invalid signature",
"message": "Signature verification failed"
}
Reference Implementation (TypeScript):
login = async (req: Request, res: Response) => {
const { w3id, session, appVersion, signature } = req.body;
if (!w3id || !session || !signature) {
return res.status(400).json({ error: "Missing required fields" });
}
// Verify signature (see Signing documentation)
const verificationResult = await verifySignature({
eName: w3id,
signature: signature,
payload: session,
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL,
});
if (!verificationResult.valid) {
return res.status(401).json({
error: "Invalid signature",
message: verificationResult.error
});
}
// Create authentication token
const token = await auth().createCustomToken(w3id);
res.status(200).json({ token });
};
Platform Implementation
Example: Blabsy Auth Controller
Version Validation Helper Function:
Before implementing the auth controller, you'll need a function to compare semantic versions. Here's an implementation example:
/**
* Compares two semantic version strings (e.g., "1.2.3")
* @param appVersion - The version to check (e.g., "0.3.5")
* @param minVersion - The minimum required version (e.g., "0.4.0")
* @returns true if appVersion >= minVersion, false otherwise
*/
function isVersionValid(appVersion: string | undefined, minVersion: string): boolean {
if (!appVersion) {
return false; // Missing version is considered invalid
}
// Parse versions into [major, minor, patch] arrays
const parseVersion = (version: string): number[] => {
return version.split('.').map(Number).slice(0, 3);
};
const app = parseVersion(appVersion);
const min = parseVersion(minVersion);
// Compare lexicographically: major, then minor, then patch
for (let i = 0; i < 3; i++) {
if (app[i] > min[i]) {
return true; // appVersion is newer
}
if (app[i] < min[i]) {
return false; // appVersion is older
}
// Continue to next component if equal
}
return true; // Versions are equal
}
Language-Agnostic Implementation:
The version comparison logic can be implemented in any language:
- Parse versions: Split version strings by '.' into arrays of integers (major, minor, patch)
- Compare components: Compare major, then minor, then patch lexicographically
- Return result: Return
trueif appVersion >= minVersion,falseotherwise
Library Alternatives:
- Node.js/TypeScript: Use
semverpackage:semver.gte(appVersion, minVersion) - Python: Use
packaging.version:Version(appVersion) >= Version(minVersion) - Go: Use
golang.org/x/mod/semver:semver.Compare(appVersion, "v"+minVersion) >= 0 - Java: Use
org.apache.maven.artifact.versioning.ComparableVersion
import { Request, Response } from "express";
import { v4 as uuidv4 } from "uuid";
import { verifySignature } from "signature-validator";
export class AuthController {
// Step 1: Generate session and return w3ds://auth URI
getOffer = async (req: Request, res: Response) => {
const url = new URL(
"/api/auth",
process.env.PUBLIC_BLABSY_BASE_URL
).toString();
const session = uuidv4();
const offer = `w3ds://auth?redirect=${url}&session=${session}&platform=blabsy`;
res.json({ uri: offer });
};
// Step 2: Verify signature and authenticate
login = async (req: Request, res: Response) => {
const { w3id, session, signature, appVersion } = req.body;
// Validate input
if (!w3id || !session || !signature) {
return res.status(400).json({ error: "Missing required fields" });
}
// Verify app version
if (!isVersionValid(appVersion, "0.4.0")) {
return res.status(400).json({
error: "App version too old"
});
}
// Verify signature
const verificationResult = await verifySignature({
eName: w3id,
signature: signature,
payload: session,
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL,
});
if (!verificationResult.valid) {
return res.status(401).json({
error: "Invalid signature",
message: verificationResult.error
});
}
// Create authentication token
const token = await auth().createCustomToken(w3id);
res.status(200).json({ token });
};
}
Example: Pictique Auth Controller
import { verifySignature } from "signature-validator";
import { signToken } from "../utils/jwt";
export class AuthController {
login = async (req: Request, res: Response) => {
const { w3id, session, signature } = req.body;
// Verify signature
const verificationResult = await verifySignature({
eName: w3id,
signature: signature,
payload: session,
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL,
});
if (!verificationResult.valid) {
return res.status(401).json({
error: "Invalid signature"
});
}
// Find or create user
let user = await this.userService.findByEname(w3id);
if (!user) {
throw new Error("User not found");
}
// Generate JWT token
const token = signToken({ userId: user.id });
res.status(200).json({
user: {
id: user.id,
ename: user.ename,
},
token,
});
};
}
Security Considerations
1. Session ID Uniqueness
- Session IDs must be cryptographically random (e.g. 128-bit random)
- Each session ID should be used only once
- Expire session IDs after a reasonable time (e.g., 5 minutes)
Why this matters: Without these protections, an attacker who intercepts a signed session ID could reuse it to authenticate as the user. This is called a replay attack. If session IDs:
- Are predictable (not cryptographically random): Attackers could guess or generate valid session IDs
- Can be reused: An intercepted signed session ID could be used multiple times to gain unauthorized access
- Don't expire: An old intercepted session ID could be used indefinitely, even after the user has logged out or changed their keys
By enforcing uniqueness, one-time use, and expiration, platforms ensure that even if a signed session ID is intercepted, it cannot be used after it expires or has already been consumed, significantly reducing the window of vulnerability.
2. Signature Replay Prevention
- Include nonces or timestamps in signed payloads
- Platforms should track used session IDs
- Reject duplicate session IDs
3. App Version Validation
Note: App version validation is temporary and will be removed after the rollout is completed. It was added because some users were on outdated eID wallet versions that handled signing differently.
- Platforms should validate app version to ensure compatibility (during the rollout period)
- Reject authentication from outdated app versions
- Provide clear error messages for version mismatches
4. Error Handling
- Never expose sensitive information in error messages
- Log verification failures for security monitoring
- Return generic errors to prevent information leakage
Troubleshooting
Common Issues
-
Session validation fails
- Check that session IDs are stored and retrieved correctly
- Verify session expiration logic
- Ensure session IDs are not reused
-
Authentication token creation fails
- Verify token generation library is configured correctly
- Check that user data is available after signature verification
- Ensure token expiration is set appropriately
-
App version validation issues (temporary - will be removed after rollout)
- Verify version comparison logic
- Check that minimum required version is correctly configured
- Ensure version strings are parsed correctly
References
- eVault — Public keys and key binding certificates
- Registry — W3ID resolution and JWKS
- eID Wallet — Key management and signing
- Signing - Signature creation and verification details
- Signature Formats - Detailed signature format documentation